<?php
/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

namespace Al\Core\Lib;

/**
 * The central class for loading and reading configuration.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 */
class Config extends ParameterHolder {

  /**
   * Bootstraps the configuration.
   *
   * Transforms the currently enabled plugins configuration array, to contain
   * more convenient paths.
   * Plugins are enabled in the app/Resources/config/plugins.yml in the same
   * form as the autoloader namespaces are defined:
   * <code>
   * Acme\Blog: ../src/Acme
   * </code>
   * The last part of the namespace (eg. Blog) should be the name of the
   * directory located at the provided path.
   * But as we usually operate on the contents of the plugin folder the path is
   * changed into:
   * <code>
   * Acme\Blog: ../src/Acme/Blog
   * </code>
   * All the paths in the application are relative to the web directory, the one
   * where the front controller (index.php) resides.
   *
   * Loads the configuration files (config.yml).<br>
   * The whole process is divided into 3 steps:
   * <pre>
   * 1. The Resources/config/config.yml file is loaded from every plugin.
   * 2. A Lib\Extension#load(); method of every plugin (eg.
   *    Acme\Blog\Lib\Extension#load) is executed, the main purpose of which
   *    is to let you load other configuration files that would override the
   *    configuration of other plugins by initiating a call to
   *    $config->loadConfig('path/to/a/config/file.yml') and/or doing any other
   *    bootstrapping that is required by your plugin.
   * 3. Loads the main application-wide app/Resources/config/config.yml
   *    configuration file which once again overrides any previously defined
   *    configuration values, hence this file has the highest priority.
   * </pre>
   */
  public function bootstrap() {
    Service::get()->when('config', function (Config $me, Service $service) {
      $me['plugin_list'] = $service['yaml']
        ->load('../app/Resources/config/plugins.yml', function (&$yml) {
          array_walk($yml, function (&$loc, $key) {
            $space = explode('\\', $key);
            $loc = realpath(rtrim($loc, '/') . '/' . $space[count($space) - 1]);
          });
        });
      $namespaces = $me['namespaces'];
      foreach ($me['plugin_list'] as $key => $loc) {
        $namespaces[$key] = $loc;
        if (is_file($filename = "$loc/Resources/config/params.yml")) {
          $me->loadParams($filename);
        }
      }
      $me['namespaces'] = $namespaces;
      if (is_file($filename = '../app/Resources/config/params.yml')) {
        $me->loadParams($filename);
      }
      foreach ($me['plugin_list'] as $loc) {
        if (is_file($filename = "$loc/Resources/config/config.yml")) {
          $me->loadConfig($filename);
        }
      }
      foreach ($me['plugin_list'] as $prefix => $loc) {
        if (is_file($service['autoload']
            ->find($cls = "$prefix\\Lib\\Extension"))) {
          $extension = new $cls;
          if (is_callable(array($extension, 'load'))) {
            $extension->load($service);
          }
        }
      }
      if (is_file($filename = '../app/Resources/config/config.yml')) {
        $me->loadConfig($filename);
      }
      $service->when('event', function (Event $event) use (&$me) {
        $event->fire('configured', array($me));
      });
    });
  }

  /**
   * Attempts to load configuration from the file at the specified location.
   *
   * @param string $path The location of the desired yaml file.
   */
  public function loadConfig($path) {
    Service::set('config', Service::get('yaml')->load($path,
      function (&$yml) {
        if (isset($yml['namespaces'])) {
          array_walk($yml['namespaces'], function (&$loc, $key) {
            $space = explode('\\', $key);
            $loc = realpath(rtrim($loc, '/') . '/' . $space[count($space) - 1]);
          });
        }
        if (isset($yml['routes'])) {
          array_walk($yml['routes'], function (&$config, $name) {
            if (!isset($config['controller'])) {
              throw new \RuntimeException(
                "No controller specified for the route: \"$name\"");
            }
            $controllerParts = array_filter(explode(':', $config['controller']),
              function ($item) { return !empty($item); });
            if (count($controllerParts) != 3) {
              throw new \RuntimeException(
                "Unknown controller \"{$config['controller']}\"");
            }
            $config['controller'] = "$controllerParts[0]\\Controller\\" .
              ucfirst($controllerParts[1]) . 'Controller';
            $config['action'] = "$controllerParts[2]Action";
            if (isset($config['path'])) {
              $config['path'] = '/' . trim($config['path'], '/');
              $params = array();
              preg_match_all('/{([^}]+)}/', $config['path'], $params);
              $config['params'] = $params[1];
            } else {
              $config['path'] = '/';
              $config['params'] = array();
            }
            $config['method'] = isset($config['method']) ?
              strtolower($config['method']) : false;
            if (!isset($config['defaults']) || !is_array($config['defaults'])) {
              $config['defaults'] = array();
            }
            if (!isset($config['requirements']) ||
                !is_array($config['requirements'])) {
              $config['requirements'] = array();
            }
            $config['cache'] = isset($config['cache']) ?
              (bool)$config['cache'] : false;
          });
        }
      }));
  }

  /**
   * Attempts to load configuration parameters from the file at the specified
   * location.
   *
   * @param string $path The location of the desired yaml file.
   */
  public function loadParams($path) {
    Service::set('config.params', Service::get('yaml')->load($path));
  }

  /**
   * Retrieves a path to the physical location of a plugin.
   *
   * Example:
   * <code>
   * #app/Resources/config/plugins.yml
   * Acme\Blog: ../src/Acme
   *
   * $config->getPluginLocation('Acme\Blog'); // ../src/Acme/Blog
   * </code>
   *
   * @param string $name A plugin name.
   * @return string The path to a folder containing the plugin or NULL if no
   *                plugin with the name $name was configured.
   */
  public function getPluginLocation($name) {
    return isset($this['plugin_list'][$name]) ? $this['plugin_list'][$name] :
      null;
  }

  /**
   * Creates a symlink for the Resources/public folder of a plugin named as
   * as a namespace with all the backslashes removed (eg. web/plugin/AcmeBlog)
   * if necessary and returns a web path to it.
   *
   * @param string $name A plugin name.
   * @return string The web root directory of a plugin.
   */
  public function getPluginWebRoot($name) {
    if (!($loc = $this->getPluginLocation($name)) ||
        !is_dir($pub = "$loc/Resources/public")) {
      return null;
    }
    @mkdir('plugin', 0777, true);
    @symlink(realpath($pub), $link = 'plugin/' . str_replace('\\', '', $name));
    return "/$link";
  }

  /**
   * Returns translated strings.
   *
   * @param string $key A translation key.
   * @param array $params [optional] Named translation params, specified as
   *                      starting with a colon in a translation string.
   * @return string A translation for the current locale.
   */
  public function trans($key, $params = array()) {
    $locale = Service::get('http.session._locale', 'en');
    $result = preg_replace_callback('/{(\w+)}/',
      function ($matches) use($params) {
        return isset($params[$matches[1]]) ? $params[$matches[1]] : $matches[1];
      }, (string)Service::get("config.lang.$locale.$key", ''));
    return $result ? $result : $key;
  }

  /**
   * Updates yaml configuration string with $content at the specified $index.
   *
   * @param string $yaml
   * @param string $key
   * @param string $content
   * @return string
   */
  public function updateConfig($yaml, $key, $content) {
    if (false === $content) return $yaml;
    if (false === $yaml) $yaml = '';
    $matches = array();
    $regex = "/([^\\S\\n]*)$key:(?:\\n\\1\\s+[^\\n]+)*/";
    preg_match($regex, $yaml, $matches);
    if ($matches) {
      $new = false;
    } else {
      $new = true;
      $matches[0] = "$key:";
      $matches[1] = '';
    }
    if (false === strpos($matches[0], $content)) {
      $matches[0] .= "\n$matches[1]  $content";
      if ($new) {
        $yaml .= (strlen($yaml) ? "\n" : '') . $matches[0];
      } else {
        $yaml = preg_replace($regex, $matches[0], $yaml);
      }
      return $yaml;
    }
    return $yaml;
  }

  /**
   * Converts a human readable file size value to a number of bytes that it
   * represents. Supports the following modifiers: K, M, G and T.
   * Invalid input is returned unchanged.
   *
   * Example:
   * <code>
   * $config->human2byte(10);          // 10
   * $config->human2byte('10b');       // 10
   * $config->human2byte('10k');       // 10240
   * $config->human2byte('10K');       // 10240
   * $config->human2byte('10kb');      // 10240
   * $config->human2byte('10Kb');      // 10240
   * // and even
   * $config->human2byte('   10 KB '); // 10240
   * </code>
   *
   * @param number|string $value
   * @return number
   */
  public function human2byte($value) {
    return preg_replace_callback('/^\s*(\d+)\s*(?:([kmgt]?)b?)?\s*$/i',
      function ($m) {
        switch (strtolower($m[2])) {
          case 't': $m[1] *= 1024;
          case 'g': $m[1] *= 1024;
          case 'm': $m[1] *= 1024;
          case 'k': $m[1] *= 1024;
        }
        return $m[1];
      }, $value);
  }

}